iT邦幫忙

2022 iThome 鐵人賽

DAY 21
1
Software Development

30 天與九頭蛇先生做好朋友系列 第 21

通知應用程式登出

  • 分享至 

  • xImage
  •  

今天將會是一個複雜的主題,因為要說明多個應用程式互相影響的情境。

首先先來完成之前沒做好的東西,remember 的設定會影響 LoginRequest 與 ConsentRequest 的 skip 值,但之前的範例程式碼並沒有跟著調整。

首先是 Login Provider 的調整:

$loginRequest = $adminApi->getLoginRequest($loginChallenge);

if ($loginRequest->getSkip()) {
    // 固定使用 LoginRequest 的 Subject
    $acceptLoginRequest = new AcceptLoginRequest([
        'subject' => $loginRequest->getSubject(),
    ]);

    $completedRequest = $adminApi->acceptLoginRequest($loginChallenge, $acceptLoginRequest);

    return Redirect::away($completedRequest->getRedirectTo());
}

之前有大概說明如果沒設定 remember=true ,skip 會是 false。參考關鍵原始碼的實作,只要 s.forwardAuthenticationRequest() 的第四個參數 subject 為空字串,在建立 LoginRequest 的時候,skip 就會設定為 false。

以下解釋 Hydra 要向使用者請求驗證的規則(也就是 skip 是 false 的原因)有幾個:

  1. 身分驗證請求「強制」要求重新做身分驗證。(使用 prompt=login 參數)
  2. Hydra 的 Login Session 不存在,這是 remember=false 的情境。(原始碼 authenticationSession() 方法實作在取 Session 的條件是 remember=true
  3. 設定 max_age 參數(單位為秒),且先前身分驗證時間至今已經超過這個設定。

若是不在上面條件的話,skip 就會為 true。

再來是 Consent Provider 的調整:

$consentRequest = $adminApi->getConsentRequest($consentChallenge);

if ($consentRequest->getSkip()) {
    // 固定使用 request 裡面的 scope
    $acceptConsentRequest = new AcceptConsentRequest([
        'grantScope' => $consentRequest->getRequestedScope(),
    ]);

    $completedRequest = $adminApi->acceptConsentRequest($consentChallenge, $acceptConsentRequest);

    return Redirect::away($completedRequest->getRedirectTo());
}

參考原始碼,其實跟 LoginRequest 很類似,會有多個情境是 Hydra 需要向使用者重新請求授權,規則如下:

  1. 身分驗證請求「強制」要求做授權。(使用 prompt=consent 參數)
  2. 若是一個 Public 的應用程式,且不是 https 的話,則 Hydra 會認為必須要重新要求授權。(理由需要看註解,大意是說:RFC 提到授權伺服器不應該自動處理授權,除非能確保應用程式的身分,因此 Client.IsPublic() 的實作是確認有設定驗證方法)
  3. Hydra 的 Consent Session 不存在,這是 remember=false 的情境。解讀原始碼 SQL 的理解是:找使用者最近一次(requested_at DESC)在應用程式主動(skip=false)同意授權,且有勾選 remember 的記錄。
  4. 當然,找到最後一次授權的記錄後,還要確定目前請求的 scope 是否跟找到的記錄相同。若應用程式請求了不同的授權範圍,則授權伺服器就需要再向使用者請求授權。

完成了之後,再實作出兩個不同的 RP 出來。Hydra 允許同個 Domain 下有兩個不同的 RP(事實上 OAuth 2.0 或 OpenID Connect 也沒有限制這件事),所以我們註冊新的兩個 RP:

hydra --endpoint http://127.0.0.1:4445/ clients --skip-tls-verify \
	create \
		--id rp1 \
		--secret secret1 \
		--grant-types authorization_code,implicit,client_credentials,refresh_token \
		--response-types "code,token,id_token,token code,code id_token,id_token token,id_token token code" \
		--scope openid \
		--token-endpoint-auth-method client_secret_basic \
		--callbacks http://127.0.0.1:8000/rp1/callback \
		--post-logout-callbacks "http://127.0.0.1:8000/rp1/logout/callback"
hydra --endpoint http://127.0.0.1:4445/ clients --skip-tls-verify \
	create \
		--id rp2 \
		--secret secret2 \
		--grant-types authorization_code,implicit,client_credentials,refresh_token \
		--response-types "code,token,id_token,token code,code id_token,id_token token,id_token token code" \
		--scope openid \
		--token-endpoint-auth-method client_secret_basic \
		--callbacks http://127.0.0.1:8000/rp2/callback \
		--post-logout-callbacks "http://127.0.0.1:8000/rp2/logout/callback"

程式的部分前幾天都說明過了,這邊就直接參考 GitHub commit

Session 控制問題

目前設計可以發現身分驗證上的一個盲點,流程是這樣:

  1. 登入 RP1
  2. 登入 RP2
  3. 登出 RP2
  4. RP1 還在登入狀態

對授權來說,使用者同意授權是使用者信任個別應用程式的關係,因此取消授權是要個別獨立取消授權的。但對身分驗證來說,會是裝置與授權伺服器的狀態,也就是 Session ID 的狀態。因此合理的行為應該是:當單點登入(SSO)完成時,所有 RP 都能共享這個登入狀態,但在單點登出(SLO)的時候,所有 RP 也需要「共享」這個狀態。

為此 OpenID Connect 定義了幾個規範,能夠在這個情境將登出狀態「共享」給其他的 RP。

設定 Back-Channel Logout

實際上登出的流程是很複雜的,本系列就只說明筆者有經驗的 Back-Channel Logout,所謂的 Back-Channel 指的是 Backend,也就是由後端來請求清除登入狀態。流程是,當 Hydra 收到登出請求的時候,再由 Hydra 通知應用程式所提供的端點清除,這個端點稱之為 backchannel_logout_uri ,每個應用程式都有各自獨立且唯一的端點。

這個 URI 是註冊應用程式的時候需要提供的,因此需要再更新一次設定(RP2 請讀者比照辦理):

hydra --endpoint http://127.0.0.1:4445/ clients --skip-tls-verify \
      update rp1 \
          --grant-types authorization_code,implicit,client_credentials,refresh_token \
          --response-types "code,token,id_token,token code,code id_token,id_token token,id_token token code" \
          --scope openid \
          --token-endpoint-auth-method client_secret_basic \
          --callbacks http://127.0.0.1:8000/rp1/callback \
          --post-logout-callbacks "http://127.0.0.1:8000/rp1/logout/callback" \
          --backchannel-logout-callback "http://127.0.0.1:8000/api/rp1/logout/backchannel" \
          --backchannel-logout-session-required true

這裡新增兩個參數,一個是 --backchannel-logout-callback 是指跟 Hydra 約定好當登出的時候要呼叫這個端點;另一個 --backchannel-logout-session-required 文件說明是指 OP 在呼叫 RP 的時候要帶 sid 的訊息,但觀察 Hydra 原始碼和實際測試,其實是沒有差別的。

實作 Back-Channel Logout

開路由,Laravel 注意要開在 api.php 裡,因為 web.php 裡面的 POST 方法預設會擋 CSRF:

Route::post('/rp1/logout/backchannel', Rp1LogoutBackChannel::class)->name('rp1.logout.backchannel');
Route::post('/rp2/logout/backchannel', Rp2LogoutBackChannel::class)->name('rp2.logout.backchannel');

Hydra 會送一個 logout_token 的參數到 Logout backchannel:

$logoutToken = $request->input('logout_token');

它也是個 JWT,內容如下:

{
  "aud": [
    "rp2"
  ],
  "events": {
    "http://schemas.openid.net/event/backchannel-logout": {}
  },
  "iat": 1665020471,
  "iss": "http://127.0.0.1:4444/",
  "jti": "371f6287-bef1-41e6-848f-fa6fe7aa1053",
  "sid": "942ab335-6857-4bb1-9616-f1e876c21d31"
}

驗證方法如下:

  • 驗證簽章,跟登入驗證 ID Token 的方法一樣。
  • aud 驗證方法跟驗證 ID Token 的方法一樣。
  • events 參數必須存在,且裡面要有 http://schemas.openid.net/event/backchannel-logout key。
  • iat 與當下時間不能差太多,可依應用程式特性決定要多久。
  • iss 驗證方法跟驗證 ID Token 的方法一樣。
  • 針對 jti 做防重送攻擊。
  • Logout Token 不會有 nonce。

驗證完成後,再來就是把 sid 拿來清除應用程式的登入狀態(如 Session)了。 說起來很簡單,但實際要做的困難點在於登入的時候必須要拿 sid 跟 session 做對應。這應該還不會很困難,不過加上明天要做的任務就會很困難了。

今天完成的程式沒有處理 Session 與驗證 Logout Token 的部分,但有開好路由,詳細請看 GitHub commit


上一篇
實作 Logout Provider
下一篇
清除所有登入狀態
系列文
30 天與九頭蛇先生做好朋友23
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言